package dpss.replicamanager;

import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;

import javax.sound.midi.Receiver;

import dpss.commons.CustomLogger;
import dpss.commons.ServerNode;
import dpss.commons.UDPMessage;
import dpss.commons.UserInput;
import dpss.replicamanager.data.ServerNodes;
import dpss.replicamanager.gui.GUI;

/**
 * The Replica manager's UDP listener thread
 * 
 * @author musab.mirza
 * 
 */
public class UDPListener extends Thread {

	private static CustomLogger logger = CustomLogger.getInstance();

	// References to the node's instance variables
	private DatagramSocket socket;
	private ServerNodes nodes;
	private GUI gui;

	public UDPListener(DatagramSocket socket, ServerNodes nodes, GUI gui) {
		this.socket = socket;
		this.nodes = nodes;
		this.gui = gui;
	}

	/**
	 * The execution method
	 */
	public void run() {

		logger.log("Started UDP listener!");

		// Start listening on UDP port
		for (;;) {

			UDPMessage messageReceived = UDPMessage.receiveUDPMessage(socket);

			switch (messageReceived.getMessage()) {

			// REQUESTS
			case "getAllLeaders":
				getAllLeaders(messageReceived);
				break;

			case "registerServerNode":
				registerServerNodeHandler(messageReceived);
				break;

			case "deregisterServerNode":
				deregisterServerNodeHandler(messageReceived);
				break;
			
			case "failNotify":
				failNotifyHandler(messageReceived);
				break;

				
			// RESPONSES OF MESSAGES ALREADY SENT OUT
			case "initializationResponse":
				initializationResponseHandler(messageReceived);
				break;
				
			case "uninitializationResponse":
				uninitializationResponseHandler(messageReceived);
				break;
				
			default:
				logger.log("ERROR - Unknown request, ignoring");
				break;
			}

		}

	}
	
	/**
	 * Handles the failure of a server node, restarts if necessary
	 * @param messageReceived
	 */
	private synchronized void failNotifyHandler(UDPMessage messageReceived) {
		
		logger.log("Failure notification received for " + ((InetSocketAddress) messageReceived.getData()).toString());
		
		ServerNode targetNode = nodes.get(  (InetSocketAddress) messageReceived.getData()  );
		targetNode.increaseFailCount();
		
		if (targetNode.getFailCount() == 4){

			targetNode.setFailCount(0);
			targetNode.setFaulty(false);
			
			// Prepare the UDP message to be sent
			HashMap<String, Object> data = new HashMap<String, Object>();
			HashMap<String, InetSocketAddress> otherLeaders = nodes
					.getOtherLeaders(targetNode.getGroup());
			data.put("otherLeaders", otherLeaders);
			data.put("serverNode", targetNode);
			data.put("myLeader", nodes.getLeaderAddress(targetNode.getGroup()));
			
			logger.log("Re-initializing the failed node (on 4th fail): " + targetNode.getSocketAddress().toString());

			// Send initialization request
			UDPMessage messageToSend = new UDPMessage("initialize", data, true,
					null);
			UDPMessage.sendMessage(socket, messageToSend,
					targetNode.getSocketAddress());

		}
		gui.refresh();
	}

	/**
	 * Removes a faulty node
	 */
	private synchronized void disconnectNode(InetSocketAddress key){
		nodes.removeByKey(key);
		UDPMessage messageToSend = new UDPMessage("disconnect", null, true, "");
		UDPMessage.sendMessage(socket, messageToSend, key);
		logger.log("Disconnected " + key.toString());
		gui.refresh();
	}

	private synchronized void uninitializationResponseHandler(UDPMessage messageReceived) {
		
		if (!messageReceived.isSuccess()) {
			GUI.showError("Something went wront with uninitialization of "+messageReceived.getSenderSocketAddress().toString()+", reason: \n"
					+ messageReceived.getErrorMessage() + "\nThis node will now be disconnected!");
			disconnectNode(messageReceived.getSenderSocketAddress());
		}
	}

	/**
	 * Handles the initialization response
	 * 
	 * @param messageReceived
	 */
	private synchronized void initializationResponseHandler(UDPMessage messageReceived) {
	
		if (!messageReceived.isSuccess()) {
			GUI.showError("Something went wront with initialization of "+messageReceived.getSenderSocketAddress().toString()+", reason: \n"
					+ messageReceived.getErrorMessage() + "\nThis node will now be disconnected!");
			disconnectNode(messageReceived.getSenderSocketAddress());
		}
	}


	/**
	 * Removes a node from the db
	 * 
	 * @param senderIp
	 */
	private synchronized void deregisterServerNodeHandler(
			UDPMessage messageReceived) {

		logger.log("Server node removal request from "
				+ messageReceived.getSenderSocketAddress().toString());

		nodes.deregister(messageReceived.getSenderSocketAddress());
		UDPMessage messageToSend = new UDPMessage("removalResponse", "", true,
				"");
		UDPMessage.sendMessage(socket, messageToSend,
				messageReceived.getSenderSocketAddress());
		gui.refresh();
	}

	/**
	 * Registers a server node with the replication manager
	 * 
	 * @param inetAddress
	 */
	private synchronized void registerServerNodeHandler(
			UDPMessage messageReceived) {

		logger.log("Server node registration request from "
				+ messageReceived.getSenderSocketAddress().toString());

		if (nodes.register(messageReceived.getSenderSocketAddress())) {
			UDPMessage messageToSend = new UDPMessage("registrationResponse",
					"", true, "");
			UDPMessage.sendMessage(socket, messageToSend,
					messageReceived.getSenderSocketAddress());
		} else {
			UDPMessage messageToSend = new UDPMessage("registrationResponse",
					"", false, "Could not register node");
			UDPMessage.sendMessage(socket, messageToSend,
					messageReceived.getSenderSocketAddress());
		}

		gui.refresh();
	}

	/**
	 * Registers the front end and returns it the list of all the leaders
	 * 
	 * @param messageReceived
	 */
	private void getAllLeaders(UDPMessage messageReceived) {

		logger.log("Get all leaders request");
		
		HashMap<String, InetSocketAddress> leaderSocketAddresses = new HashMap<String, InetSocketAddress>();
		for (ServerNode n : nodes.getAllLeaders()){
			leaderSocketAddresses.put(n.getGroup(), n.getSocketAddress());
		}
		UDPMessage messageToSend = new UDPMessage("leaderSocketAddresses", leaderSocketAddresses,
				true, "");
		UDPMessage.sendMessage(socket, messageToSend,
				messageReceived.getSenderSocketAddress());

	}

}
